/* 
 * File:   Disk.cpp
 * Author: chris
 *
 * Created on January 25, 2009, 1:18 PM
 * Modified; Feb 3, 2012; CGP
 *      Changes to have use physical disk device. 
 *      Apparently Mac OS X can use lseek for long seeks. 
 *          (http://lists.apple.com/archives/unix-porting/2002/Jul/msg00099.html)
 */

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <stdlib.h>
#include "Disk.h"

#ifdef LINUX
#include <linux/fs.h>
#endif

#ifdef MACOSX
#include <sys/disk.h>
#endif

#include <fcntl.h>
#include <sys/ioctl.h>

#include <iostream>
using namespace std;

#include "BadNumberOfBlocksException.h"
#include "CannotReadException.h"

unsigned long Disk::numberOfBlocks() {
    return m_numberOfBlocks;
}

unsigned int Disk::blockSize() {
    return m_blockSize;
}


void Disk::GetSizes(const char *device, unsigned long &numberOfBlocks, unsigned int &blockSize) {
    struct stat sb;

    if (stat(device, &sb) == -1) {
      perror("stat");
      exit(EXIT_FAILURE);
    }

    blockSize = sb.st_blksize;
    printf("Disk::GetSizes: returned block size= %d\n", blockSize);

    // Get the number of blocks

    int fd = open(device, O_RDONLY);
    if (-1 == fd) {
	    printf("ERROR: Could not not open device: %s\n", device);
	    exit(EXIT_FAILURE);
    } 

#ifdef LINUX
#define IOCTL_BLOCK_SIZE 512
    unsigned long numberOfBlocksLinux;
    // this obtains the number of 512 byte blocks on the device
    int result = ioctl(fd, BLKGETSIZE, &numberOfBlocksLinux);
    if (-1 == result) {
	    // not sure if this is correct error return value
	    printf("Disk::GetSizes: ERROR: Could not do ioctl on %s\n", device);
	    exit(EXIT_FAILURE);
    }

    // Convert to the blocksize given by stat.
    if (blockSize < IOCTL_BLOCK_SIZE) {
        printf("Disk::GetSizes: ERROR: Yikes, the stat block size was less than 512\n");
        exit(EXIT_FAILURE);
    }
    
    // Compute the number of blocks in units given by stat
    numberOfBlocks = numberOfBlocksLinux/(blockSize/IOCTL_BLOCK_SIZE);
    printf("Disk::GetSizes: %lu blocks at block size= %d\n", numberOfBlocksLinux, IOCTL_BLOCK_SIZE);
#endif
    
#ifdef MACOSX
    // See p. 1347 of Mac OS X Internals, by Amit Singh, 2007
    u_int64_t numberOfBlocksMac;
    if (ioctl(fd, DKIOCGETBLOCKCOUNT, &numberOfBlocksMac) < 0) {
        printf("Disk::GetSizes: MacOS X ioctl failed\n");
        exit(EXIT_FAILURE);
    } else {
        printf("Disk::GetSizes: MacOS X block count= %lu\n", (unsigned long) numberOfBlocksMac);
    }
	numberOfBlocks = numberOfBlocksMac;
	
	u_int32_t blockSizeMac;
    if (ioctl(fd, DKIOCGETBLOCKSIZE, &blockSizeMac) < 0) {
        printf("Disk::GetSizes: MacOS X ioctl failed\n");
        exit(EXIT_FAILURE);
    } else {
        printf("Disk::GetSizes: MacOS X block size= %u\n", (unsigned) blockSizeMac);
    }	

	// Convert to the blocksize given by stat.
    if (blockSize < blockSizeMac) {
        printf("Disk::GetSizes: ERROR: Yikes, the stat block size was less than 512\n");
        exit(EXIT_FAILURE);
    }
	
	numberOfBlocks = numberOfBlocksMac/(blockSize/blockSizeMac);
#endif
    
    if (-1 == close(fd)) {
	    printf("Disk::GetSizes: ERROR: Could not close device %s\n", device);
	    exit(EXIT_FAILURE);
    }

    printf("Disk::GetSizes: %lu blocks at returned block size= %d\n", numberOfBlocks, blockSize);
}

long Disk::Seek(unsigned long offset) {
    unsigned long result;

    // Seek into the device
#ifdef LINUX
    result = lseek64(m_fd, offset, SEEK_SET);
#else
    result = lseek(m_fd, offset, SEEK_SET);
#endif
    return result;
}


Disk::Disk(const char *deviceName, unsigned long numberOfBlocks, unsigned int blockSize, 
    bool physicalDevice) {

    m_debugInfo = false;
    m_deviceName = deviceName;
    m_numberOfBlocks = numberOfBlocks;
    m_blockSize = blockSize;

    if ((numberOfBlocks <= 0) || (blockSize <= 0)) {
        printf("Disk.Disk: ERROR: bad number of blocks or block size\n");
        throw BadNumberOfBlocksException();
    }

    // If we are using a physical device, then only open the device-- don't attempt to create
    // the file.
    if (physicalDevice) {
        m_fd = open(deviceName, O_RDWR);
    } else {
        m_fd = open(deviceName, O_RDWR | O_CREAT, S_IRWXU);
    }

    if (-1 == m_fd) {
        printf("Disk.Disk: ERROR: Cannot open disk: %s\n", deviceName);
        throw CannotReadException();
    }
}


void Disk::Debug(bool state) {
    m_debugInfo = state;
}


bool Disk::Format() {
	
	// Format our file (simulated) device by writing out empty blocks.

    // First need to seek to the start.
    if (Seek(0) == -1) {
        printf("Disk.Format: ERROR: Could not seek into device.\n");
        return false;
    }
    
    // Create an empty block of unsigned chars (bytes)
    unsigned char* block = new unsigned char[m_blockSize];
    for (unsigned int i=0; i < m_blockSize; i++) {
        block[i] = 0; // initialize the block with 0 values
    }

    // Write the empty blocks out
    for (unsigned long i=0; i < m_numberOfBlocks; i++) {
		// write the block
        if ((unsigned int) write(m_fd, (void*) block, m_blockSize) != m_blockSize) { 
            delete [] block;
            printf("Disk.Format: ERROR: Could not write block to file\n");
            printf("Disk.Format: ERROR: Problem writing block number: %lu\n", i);
            return false;
        }
    }

    // All is well-- we got through the disk formatting steps.
    printf("Disk.Format: SUCCESS: Formatted disk with %lu", m_numberOfBlocks);
    printf(" blocks, each of size = %u bytes.\n", m_blockSize);
    delete [] block;
	
    return true;
}


bool Disk::Close() {
	
    if (close(m_fd) == -1) {
        printf("Disk.Close: ERROR: Could not close file\n");
        return false;
    }

    printf("Disk.Close: SUCCESS: Closed disk!\n");
	
    return true;
}


bool Disk::WriteBlock(unsigned int blockNumber, unsigned char* block) {
    unsigned long result;
	
    if (((int) blockNumber < 0) || (blockNumber >= m_numberOfBlocks)) {
        printf("Disk.WriteBlock: ERROR: block number incorrect\n");
        return false;
    }

    // Compute the byte offset into the device for the block we are going to write.
    unsigned long int offset = blockNumber * m_blockSize;

    // Seek into the device
    result = Seek(offset);
    if (result != offset) {
        printf("Disk.WriteBlock: ERROR: Could not seek into device.\n");
        return false;
    }

    if ((unsigned int) write(m_fd, (void *) block, m_blockSize) != m_blockSize) {
        printf("Disk.WriteBlock: ERROR: block write failed\n");
        return false;
    }

    if (m_debugInfo) {
       printf("Disk.WriteBlock: SUCCESS: Wrote disk block to disk.\n");
    }
	
    return true;
}

bool Disk::CheckBlock(unsigned int blockNumber) {

    // Compute the necessary offset into the file for the block.
    long offset = blockNumber * m_blockSize;

    	struct stat statinfo;
	long fileSizeInBytes;

	// Now, figure out the actual number of bytes in the file.
	int result = fstat(m_fd, &statinfo);
	if (-1 == result) {
		fprintf(stderr, "Disk::CheckBlock: ERROR: Could not fstat disk\n");
		exit(1);
	}

	fileSizeInBytes = statinfo.st_size;
	printf("Disk::CheckBlock: disk has %ld bytes\n", fileSizeInBytes);


    // If the offset needed for the block is >= the fileSizeInBytes, then
    // this is not a valid block number. Block 0 starts at byte 0 of the file,
    // block 1 starts at m_blockSize offset, and ..., block N starts at N * m_blockSize
    // offset.
    // E.g., say blockNumber (parameter to this method) is 5, and m_blockSize is 512, 
    // and the file only has blocks 0 through 4. 
    // The number of bytes in the file would be 5 * 512 = 2560 bytes.
    // The offset for a (error) block number 5 would be 5 * 512 = 2560.
    // Thus, the offset for the error block (2560) >= file size in bytes (2560).
    if (offset >= fileSizeInBytes) {
        // error
        return false;
    }
    return true;
}


bool Disk::ReadBlock(unsigned int blockNumber, unsigned char *block) {
    long result;

	
    if (((int)blockNumber < 0) || (blockNumber >= m_numberOfBlocks)) {
        printf("Disk.ReadBlock: ERROR: block number incorrect\n");
        return false;
    }

    // Compute the offset into the file for the block we are going to read.
    long offset = blockNumber * m_blockSize;

    if (m_debugInfo) {
        printf("Disk.ReadBlock: offset= %ld\n", offset);
    }

    result = Seek(offset);
    if (offset != result) {
        printf("Disk.ReadBlock: ERROR: Could not seek into device.\n");
        return false;
    }

    //printf("Here1"); fflush(stdout);

    result = read(m_fd, (void *) block, m_blockSize);
    if ((unsigned int) result != m_blockSize) {
        printf("Disk.ReadBlock: ERROR: block read failed; read result= %lu\n", result);
        printf("Disk.ReadBlock: ERROR: attempting to read number of bytes= %u\n", m_blockSize);
        return false;
    }

    //printf("Here2"); fflush(stdout);

    if (m_debugInfo) {
        printf("Disk.ReadBlock: read %d bytes\n.", m_blockSize);
    }

    return true;
}

#define DEVICE_NAME_LENGTH  100

void Disk::Test() {
    //char *s = NULL;
    int cmd = 0;
    Disk *disk1 = NULL;
    int blockNumber = 0;
    int byteToWrite;
    unsigned char *block = NULL;
    bool physicalDevice = false;
    unsigned int blockSize = Disk::DEFAULT_BLOCK_SIZE;
    char deviceName[DEVICE_NAME_LENGTH] = "disk1";
    unsigned long numberOfBlocks = 1000;

    for (;;) {
        printf("\nmain: Enter command:\n");
        printf("   0) Exit program\n");
        printf("   1) Use physical device (default is to use simulated disk in a file)\n");
        printf("   2) Open disk object-- named: %s\n", deviceName);
        printf("   3) Reformat disk-- named: %s\n", deviceName);
        printf("   4) Close disk-- named: %s (keep contents)\n", deviceName);
        printf("   5) Write disk block to open disk\n");
        printf("   6) Read disk block from open disk\n");

        printf("Enter command: ");
        if (scanf("%d", &cmd) != 1) {
            printf("ERROR: Couldn't read from user\n"); 
	        continue;
        }

        switch (cmd) {
            case 0:
                return;

            case 1:
                printf("Enter name of physical disk device: ");

                // Need this, otherwise the getline doesn't block. Seems to be the case that
                // the prior scanf doesn't get rid of the newline at the end of the input buffer
                // and this throws that away.
                cin.ignore();

                cin.getline(deviceName, DEVICE_NAME_LENGTH);
                physicalDevice = true;
                Disk::GetSizes(deviceName, numberOfBlocks, blockSize);
                break;

            case 2: // create and open disk1
                if (disk1 != NULL) {
                    printf("main: ERROR: disk1 is open!n");
                    break;
                }

                disk1 = new Disk(deviceName, numberOfBlocks, blockSize, physicalDevice);
                block = new unsigned char[blockSize];
                printf("main: SUCCESS: disk1 is now open.");
                break;

            case 3: // reformat disk1
                if (disk1 == NULL) {
                    printf("main: ERROR: disk1 is not open!\n");
                    break;
                }

                if (disk1->Format()) {
                    printf("main: SUCCESS: formatted disk1!\n");
                } else {
                    printf("main: ERROR: could not format disk1!\n");
                }
                break;

            case 4: // close disk1 (don't reformat)
                if (disk1 != NULL) {
                    if (disk1->Close()) {
                        printf("main: SUCCESS: closed disk1!\n");
                    } else {
                        printf("main: ERROR: could not close disk1!\n");
                    }
                } else {
                    printf("main: ERROR: disk not open!\n");
                    break;
                }

                disk1 = NULL;
                printf("main: SUCCESS: disk1 closed\n");
                break;

            case 5: // write disk block to open disk
                if (disk1 == NULL) {
                    printf("main: ERROR: disk1 is not open!\n");
                    break;
                }

                printf("Enter block number to write: ");
                if (scanf("%d", &blockNumber) != 1) {
		            printf("ERROR: Couldn't read block number from user\n");
                    break;
	            }

                printf("Enter integer value to write, as a test, to all bytes of block: ");
                if (scanf("%d", &byteToWrite) != 1) {
		            printf("ERROR: Couldn't read byte to write from user\n");
		            break;
	            }

                printf("main: Attempting to write block %d\n", blockNumber);

                for (unsigned int i=0; i < disk1->blockSize(); i++) {
                    block[i] = byteToWrite;
                }

                if (! disk1->WriteBlock(blockNumber, block)) {
                    printf("main: ERROR: Block write failed!\n");
                } else {
                    printf("main: SUCCESS: Block written!\n");
                }
                break;

            case 6: // read disk block from open disk
                if (disk1 == NULL) {
                    printf("main: ERROR: disk1 is not open!\n");
                    break;
                }

                printf("Enter block number to read: ");
                if (scanf("%d", &blockNumber) != 1) {
		            printf("ERROR: Couldn't read block number from user\n");
		            break;
	            }
                printf("main: Attempting to read block %d\n", blockNumber);

                if (! disk1->ReadBlock(blockNumber, block)) {
                    printf("main: ERROR: Block read failed!\n");
                } else {
                    printf("main: SUCCESS: Block read!\n");
                    for (unsigned int i=0; i < disk1->blockSize(); i++) {
                        printf("%d,", block[i]);
                    }
                    printf("\n");
                }
                break;

            default:
                printf("main: Bad command!\n");
                break;
        } // end switch
    } // end for
} // end Test()



